#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
import sys
import time
import enum
import logging
from scapy.all import Ether

sys.path.append("/home/p4/p4_load_balance/utils")
import p4runtime_sh.shell as sh
import p4runtime_shell_utils as shu

logger = logging.getLogger(None)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
# create formatter and add it to the handlers
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
ch.setFormatter(formatter)
logger.addHandler(ch)


class PuntReason(enum.Enum):
    UNRECOGNIZED_OPCODE = 1
    OPERATION_RESPONSE = 2
    ACTIVE_REQUEST = 3


class ControllerOptcode(enum.Enum):
    NO_OP = 0
    READ_CONGESTION_REG = 1
    WRITE_CONGESTION_REG = 2
    READ_FLOWTABLE_REG = 3
    RESET_FLOWTABLE_REG = 4
    WRITE_FLOWTABLE_REG = 5
    GENERATE_SPROBE = 6
    UPDATE_CONGESTION_MSG = 7


def int_to_hex(num):
    hex_str = hex(num)
    if len(hex_str[2:]) < 8:
        return "0x" + ".".join(["0" for i in range(8 - len(hex_str[2:]))]) + hex_str[2:]

    return hex_str


def ip_to_decimal(ip):
    bytes = list(map(int, ip.split(".")))
    return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]


def get_exp_num_packetins(pktin, exp_num_packets, timeout_sec):
    """
    exp_num_packets
    timeout_spec
    @return: pktlist: 在pktin上接收到的报文
    """
    pktlist = []
    pktin.sniff(lambda p: pktlist.append(p), timeout=timeout_sec)
    return pktlist


# TODO: 改进，利用python新特性
class S_Probe:

    def __init__(self, srcIp, dstIp, route_index, direction, has_metadata):
        self.srcIp = srcIp
        self.dstIp = dstIp
        self.route_index = route_index
        self.direction = direction
        self.has_metadata = has_metadata


class Controller:
    CPU_PORT = 510

    def __init__(self, p4info_txt_fname, p4prog_binary_fname, roll_poll_interval):
        """
        完成初始化工作，读取后缀为`.json`以及`.p4info.txtpb`格式的文件
        """
        # 判断文件是否存在

        # 初始化
        logging.info("start controller init.")
        self.roll_poll_time = roll_poll_interval
        self.p4info_txt_fname = p4info_txt_fname
        self.p4prog_binary_fname = p4prog_binary_fname

        p4info_data = shu.read_p4info_txt_file(p4info_txt_fname=p4info_txt_fname)

        # Create Python dicts from name to integer values, and integer
        # values to names, for the P4_16 serializable enum types
        # PuntReason_t and ControllerOpcode_t once here during setup.
        self.punt_reason_name2int, self.punt_reason_int2name = (
            shu.serializable_enum_dict(p4info_data, "PuntReason_t")
        )
        self.opcode_name2int, self.opcode_int2name = shu.serializable_enum_dict(
            p4info_data, "ControllerOpcode_t"
        )

        self.p4info_obj_map = shu.make_p4info_obj_map(p4info_data)
        self.cpm_packetin_id2data = shu.controller_packet_metadata_dict_key_id(
            self.p4info_obj_map, "packet_in"
        )

    def addController(
        self,
        device_id: int,
        grpc_addr: str,
        verbose: bool = False,
    ):
        global sleep_time

        # 建立连接
        sh.setup(
            device_id=device_id,
            grpc_addr=grpc_addr,
            election_id=(0, 6),
            config=sh.FwdPipeConfig(self.p4info_txt_fname, self.p4prog_binary_fname),
            verbose=verbose,
        )

        # TODO: 完成交换机初始表项的添加和寄存器初始键值对的添加
        from switch_controller import main

        main(
            sw_name_to_thrift_port="/home/p4/p4_load_balance/S_Probe/build/sw_name_to_thrift_port.csv"
        )
        time.sleep(2)

        # 生成初始S_Probe探测报文
        generate_s_probe(
            sh.PacketOut(),
            S_Probe(ip_to_decimal("10.0.0.11"), ip_to_decimal("10.1.0.11"), 0, 0, 0),
        )

        pktin = sh.PacketIn()

        # 轮询间隔roll_poll_interval
        while True:

            pktlist = get_exp_num_packetins(pktin, timeout_sec=1)
            if len(pktlist) >= 1:
                pktdata = shu.decode_packet_in_metadata(
                    self.cpm_packetin_id2data, pktlist[0].packet
                )
                # 经过上面处理得到的pktdata为一个字典类型，key构成的集合为 (metadata, payload)
                # 其中metadata也是一个字典，里面又包含属性 (input_port, punt_reason, opcode, operand0, operand1, operand2, operand3)
                # analyse_pktdata(pktdata['metadata'],sh.PacketOut())
                pkt_metadata = pktdata["metadata"]
                if (
                    pkt_metadata["punt_reason"] == PuntReason.ACTIVE_REQUEST.value
                    and pkt_metadata["opcode"]
                    == ControllerOptcode.UPDATE_CONGESTION_MSG.value
                ):
                    pass
                    logging.debug(
                        f"receing update congestion message {pkt_metadata["operand0"]},{pkt_metadata["operand1"]},{pkt_metadata["operand2"]},{pkt_metadata["operand3"]},{pkt_metadata["operand4"]}"
                    )

                    # TODO: 处理消息，同时将消息添加到Redis数据库当中

            # 订阅Redis数据库消息

            # 休息时间
            logging.debug("经过一轮监听")
            time.sleep(self.roll_poll_time)


def generate_s_probe(pktout: sh.PacketOut, s_probe: S_Probe, pktmetadata=""):
    # 编辑pktout的元数据时值的类型为str类型
    pktout.metadata["opcode"] = str(ControllerOptcode.GENERATE_SPROBE.value)
    pktout.metadata["operand0"] = str(s_probe.srcIp)
    pktout.metadata["operand1"] = str(s_probe.dstIp)
    pktout.metadata["operand2"] = str(s_probe.route_index)
    pktout.metadata["operand3"] = str(s_probe.direction)
    pktout.metadata["operand4"] = str(s_probe.has_metadata)
    pktout.send()


if __name__ == "__main__":
    p4info_txt_fname = "./build/edge_switch.p4.p4info.txtpb"
    p4prog_binary_fname = "./build/edge_switch.json"
    roll_poll_interval = 1000

    controller = Controller(
        p4info_txt_fname, p4prog_binary_fname, roll_poll_interval=roll_poll_interval
    )

    # 为s0-e0部署控制器
    controller.addController(
        device_id=6,
        grpc_addr="localhost:50057",
        verbose=False,
    )
    # 为s1-e0部署控制器
    # controller.addController(
    #     device_id=10,
    #     grpc_addr="localhost:50061",
    #     verbose=False,
    # )
